﻿//StorageHelper.cs
//Copyright (c) 2013 StockSharp LLC, all rights reserved.
//This code module is part of StockSharp library.
//This code is licensed under the GNU GENERAL PUBLIC LICENSE Version 3.
//See the file License.txt for the license details.
//More info on: http://stocksharp.com

namespace StockSharp.Algo.Storages
{
	using System;
	using System.Collections;
	using System.Collections.Generic;
	using System.Diagnostics;
	using System.Linq;

	using Ecng.Collections;
	using Ecng.Common;
	using Ecng.ComponentModel;

	using StockSharp.Algo.Candles;
	using StockSharp.BusinessEntities;

	/// <summary>
	/// Вспомогательный класс для работы с внешнем хранилищем.
	/// </summary>
	public static class StorageHelper
	{
		private sealed class RangeEnumerable<TData> : SimpleEnumerable<TData>, IEnumerableEx<TData>
		{
			[DebuggerDisplay("From {_from} Cur {_currDate} To {_to}")]
			private sealed class RangeEnumerator : IEnumerator<TData>
			{
				private DateTime _currDate;
				private readonly IMarketDataStorage<TData> _storage;
				private readonly DateTime _from;
				private readonly DateTime _to;
				private readonly Func<TData, DateTime> _getTime;
				private IEnumerator<TData> _current;

				private bool _checkBounds;
				private readonly Range<DateTime> _bounds;

				public RangeEnumerator(IMarketDataStorage<TData> storage, DateTime from, DateTime to, Func<TData, DateTime> getTime)
				{
					if (storage == null)
						throw new ArgumentNullException("storage");

					if (getTime == null)
						throw new ArgumentNullException("getTime");

					if (from > to)
						throw new ArgumentOutOfRangeException("from");

					_storage = storage;
					_from = from;
					_to = to;
					_getTime = getTime;
					_currDate = from.Date;

					_checkBounds = true; // проверяем нижнюю границу
					_bounds = new Range<DateTime>(from, to);
				}

				void IDisposable.Dispose()
				{
					Reset();
				}

				bool IEnumerator.MoveNext()
				{
					if (_current == null)
					{
						_current = _storage.Load(_currDate).GetEnumerator();
					}

					while (true)
					{
						if (!_current.MoveNext())
						{
							_current.Dispose();

							var canMove = false;

							while (!canMove)
							{
								_currDate += TimeSpan.FromDays(1);

								if (_currDate > _to)
									break;

								_checkBounds = _currDate == _to.Date;

								_current = _storage.Load(_currDate).GetEnumerator();

								canMove = _current.MoveNext();
							}

							if (!canMove)
								return false;
						}

						if (!_checkBounds)
							break;

						do
						{
							var time = _getTime(Current);

							if (_bounds.Contains(time))
								return true;

							if (time > _to)
								return false;
						}
						while (_current.MoveNext());
					}

					return true;
				}

				public void Reset()
				{
					if (_current != null)
					{
						_current.Dispose();
						_current = null;
					}

					_checkBounds = true;
					_currDate = _from;
				}

				public TData Current
				{
					get { return _current.Current; }
				}

				object IEnumerator.Current
				{
					get { return Current; }
				}
			}

			private readonly IMarketDataStorage<TData> _storage;
			private readonly DateTime _from;
			private readonly DateTime _to;

			public RangeEnumerable(IMarketDataStorage<TData> storage, DateTime from, DateTime to, Func<TData, DateTime> getTime)
				: base(() => new RangeEnumerator(storage, from, to, getTime))
			{
				_storage = storage;
				_from = from;
				_to = to;
			}

			private int? _count;

			int IEnumerableEx<TData>.Count
			{
				get
				{
					if (_count == null)
					{
						// TODO
						//if (_from.TimeOfDay != TimeSpan.Zero || _to.TimeOfDay != TimeSpan.Zero)
						//	throw new InvalidOperationException("Невозможно вычислить количество элементов для диапазона со временем. Можно использовать только диапазон по датами.");

						var count = 0;

						for (var i = _from; i <= _to; i += TimeSpan.FromDays(1))
							count += _storage.Load(i).Count;

						_count = count;
					}

					return (int)_count;
				}
			}
		}

		/// <summary>
		/// Добавить хранилище свечек.
		/// </summary>
		/// <typeparam name="TCandle">Тип свечки.</typeparam>
		/// <typeparam name="TArg">Тип параметра свечки.</typeparam>
		/// <param name="storageRegistry">Внешнее хранилище.</param>
		/// <param name="candleStorage">Хранилище свечек.</param>
		/// <param name="arg">Параметр свечки.</param>
		public static void RegisterCandleStorage<TCandle, TArg>(this IStorageRegistry storageRegistry, IMarketDataStorage<Candle> candleStorage, TArg arg)
			where TCandle : Candle
		{
			storageRegistry.ThrowIfNull().RegisterCandleStorage(candleStorage, typeof(TCandle), arg);
		}

		/// <summary>
		/// Получить хранилище свечек.
		/// </summary>
		/// <typeparam name="TCandle">Тип свечки.</typeparam>
		/// <typeparam name="TArg">Тип параметра свечки.</typeparam>
		/// <param name="storageRegistry">Внешнее хранилище.</param>
		/// <param name="security">Инструмент.</param>
		/// <param name="arg">Параметр свечки.</param>
		/// <returns>Хранилище свечек.</returns>
		public static IMarketDataStorage<Candle> GetCandleStorage<TCandle, TArg>(this IStorageRegistry storageRegistry, Security security, TArg arg)
			where TCandle : Candle
		{
			return storageRegistry.GetCandleStorage<TCandle, TArg>(security, arg, storageRegistry.ThrowIfNull().DefaultDrive);
		}

		/// <summary>
		/// Получить хранилище свечек.
		/// </summary>
		/// <typeparam name="TCandle">Тип свечки.</typeparam>
		/// <typeparam name="TArg">Тип параметра свечки.</typeparam>
		/// <param name="storageRegistry">Внешнее хранилище.</param>
		/// <param name="security">Инструмент.</param>
		/// <param name="arg">Параметр свечки.</param>
		/// <param name="drive">Хранилище (база данных, файл и т.д.).</param>
		/// <returns>Хранилище свечек.</returns>
		public static IMarketDataStorage<Candle> GetCandleStorage<TCandle, TArg>(this IStorageRegistry storageRegistry, Security security, TArg arg, IMarketDataDrive drive)
			where TCandle : Candle
		{
			return storageRegistry.ThrowIfNull().GetCandleStorage(typeof(TCandle), security, arg, drive);
		}

		/// <summary>
		/// Получить хранилище свечек.
		/// </summary>
		/// <param name="storageRegistry">Внешнее хранилище.</param>
		/// <param name="series">Серия свечек.</param>
		/// <param name="drive">Хранилище (база данных, файл и т.д.).</param>
		/// <returns>Хранилище свечек.</returns>
		public static IMarketDataStorage<Candle> GetCandleStorage(this IStorageRegistry storageRegistry, CandleSeries series, IMarketDataDrive drive)
		{
			if (series == null)
				throw new ArgumentNullException("series");

			return storageRegistry.ThrowIfNull().GetCandleStorage(series.CandleType, series.Security, series.Arg, drive);
		}

		private static IStorageRegistry ThrowIfNull(this IStorageRegistry storageRegistry)
		{
			if (storageRegistry == null)
				throw new ArgumentNullException("storageRegistry");

			return storageRegistry;
		}

		internal static IEnumerable<Range<DateTime>> GetRanges<TValue>(this IMarketDataStorage<TValue> storage)
		{
			if (storage == null)
				throw new ArgumentNullException("storage");

			var range = GetRange(storage, null, null);

			if (range == null)
				return Enumerable.Empty<Range<DateTime>>();

			return storage.Dates.GetRanges(range.Min, range.Max);
		}

		/// <summary>
		/// Создать итерационный загрузчик маркет-данных для диапазона времени.
		/// </summary>
		/// <typeparam name="TData">Тип данных.</typeparam>
		/// <param name="storage">Хранилище маркет-данных.</param>
		/// <param name="from">Время начала, с которого необходимо загружать данные. Если значение не указано, то будут загружены данные с начальной даты <see cref="GetFromDate{TData}"/>.</param>
		/// <param name="to">Время окончания, до которого включительно необходимо загружать данные. Если значение не указано, то будут загружены данные до конечной даты <see cref="GetToDate{TData}"/> включительно.</param>
		/// <returns>Итерационный загрузчик маркет-данных.</returns>
		public static IEnumerableEx<TData> Load<TData>(this IMarketDataStorage<TData> storage, DateTime? from = null, DateTime? to = null)
		{
			var range = GetRange(storage, from, to);

			return range == null
				? Enumerable.Empty<TData>().ToEx()
				: new RangeEnumerable<TData>(storage, range.Min, range.Max, ((IMarketDataStorageInfo<TData>)storage).GetTime);
		}

		/// <summary>
		/// Удалить маркет-данные из хранилища для заданного периода.
		/// </summary>
		/// <typeparam name="TData">Тип данных.</typeparam>
		/// <param name="storage">Хранилище маркет-данных.</param>
		/// <param name="from">Время начала, с которого необходимо удалять данные. Если значение не указано, то будут удалены данные с начальной даты <see cref="GetFromDate{TData}"/>.</param>
		/// <param name="to">Время окончания, до которого включительно необходимо удалять данные. Если значение не указано, то будут удалены данные до конечной даты <see cref="GetToDate{TData}"/> включительно.</param>
		public static void Delete<TData>(this IMarketDataStorage<TData> storage, DateTime? from = null, DateTime? to = null)
		{
			if (storage == null)
				throw new ArgumentNullException("storage");

			var range = GetRange(storage, from, to);

			if (range == null)
				return;

			var date = range.Min.Date;

			var info = (IMarketDataStorageInfo<TData>)storage;

			while (date <= range.Max)
			{
				if (range.Max <= date && range.Max >= (date + TimeSpan.FromDays(1)))
					storage.Delete(date);
				else
				{
					var data = storage.Load(date).ToList();
					data.RemoveWhere(d => !range.Contains(info.GetTime(d)));
					storage.Delete(data);
				}

				date = date.AddDays(1);
			}
		}

		internal static Range<DateTime> GetRange<TData>(this IMarketDataStorage<TData> storage, DateTime? from, DateTime? to)
		{
			if (storage == null)
				throw new ArgumentNullException("storage");

			if (from > to)
				throw new ArgumentOutOfRangeException("to", to, "Значение to не может быть меньше значения from.");

			if (storage.Dates.IsEmpty())
				return null;

			var first = storage.Dates.First();
			var last = storage.Dates.Last() + TimeSpanHelper.LessOneDay;

			return new Range<DateTime>(first, last).Intersect(new Range<DateTime>((from ?? first).Truncate(), (to ?? last).Truncate()));
		}

		/// <summary>
		/// Получить начальную дату, с которой храняться маркет-данные в хранилище.
		/// </summary>
		/// <typeparam name="TData">Тип данных.</typeparam>
		/// <param name="storage">Хранилище маркет-данных.</param>
		/// <returns>Начальная дата. Если значение не инициализировано, значит хранилище пустое.</returns>
		public static DateTime? GetFromDate<TData>(this IMarketDataStorage<TData> storage)
		{
			if (storage.Dates.Any())
				return storage.Dates.First();
			else
				return null;
		}

		/// <summary>
		/// Получить конечную дату, по которую храняться маркет-данные в хранилище.
		/// </summary>
		/// <typeparam name="TData">Тип данных.</typeparam>
		/// <param name="storage">Хранилище маркет-данных.</param>
		/// <returns>Конечная дата. Если значение не инициализировано, значит хранилище пустое.</returns>
		public static DateTime? GetToDate<TData>(this IMarketDataStorage<TData> storage)
		{
			if (storage.Dates.Any())
				return storage.Dates.Last();
			else
				return null;
		}

		private static readonly SecurityChangeTypes _allTypes = Enumerator.JoinMask<SecurityChangeTypes>();

		private static readonly SynchronizedDictionary<Security, SynchronizedDictionary<SecurityChangeTypes, object>> _prevChanges = new SynchronizedDictionary<Security, SynchronizedDictionary<SecurityChangeTypes, object>>();

		/// <summary>
		/// Получить новые изменения инструмента.
		/// </summary>
		/// <param name="security">Инструмент.</param>
		/// <returns>Новые изменения инструмента.</returns>
		public static IEnumerable<SecurityChange> GetNewChanges(this Security security)
		{
			return security.GetNewChanges(_allTypes);
		}

		/// <summary>
		/// Получить новые изменения инструмента.
		/// </summary>
		/// <param name="security">Инструмент.</param>
		/// <param name="requiredTypes">Типы изменений, которые необходимо получить.</param>
		/// <returns>Новые изменения инструмента.</returns>
		public static IEnumerable<SecurityChange> GetNewChanges(this Security security, SecurityChangeTypes requiredTypes)
		{
			if (security == null)
				throw new ArgumentNullException("security");

			var time = security.GetMarketTime();
			
			var newChanges = new List<SecurityChange>();

			var prevChanges = _prevChanges.SafeAdd(security);

			lock (prevChanges.SyncRoot)
			{
				using (security.BeginRead())
				{
					TryAddPriceChange(security, time, prevChanges, newChanges, requiredTypes, SecurityChangeTypes.OpenPrice, security.OpenPrice);
					TryAddPriceChange(security, time, prevChanges, newChanges, requiredTypes, SecurityChangeTypes.HighPrice, security.HighPrice);
					TryAddPriceChange(security, time, prevChanges, newChanges, requiredTypes, SecurityChangeTypes.LowPrice, security.LowPrice);
					TryAddPriceChange(security, time, prevChanges, newChanges, requiredTypes, SecurityChangeTypes.ClosePrice, security.ClosePrice);

					TryAddPriceChange(security, time, prevChanges, newChanges, requiredTypes, SecurityChangeTypes.MinPrice, security.MinPrice);
					TryAddPriceChange(security, time, prevChanges, newChanges, requiredTypes, SecurityChangeTypes.MaxPrice, security.MaxPrice);

					TryAddPriceChange(security, time, prevChanges, newChanges, requiredTypes, SecurityChangeTypes.ImpliedVolatility, security.ImpliedVolatility);
					TryAddPriceChange(security, time, prevChanges, newChanges, requiredTypes, SecurityChangeTypes.HistoricalVolatility, security.HistoricalVolatility);
					TryAddPriceChange(security, time, prevChanges, newChanges, requiredTypes, SecurityChangeTypes.TheorPrice, security.TheorPrice);
					
					TryAddPriceChange(security, time, prevChanges, newChanges, requiredTypes, SecurityChangeTypes.Delta, security.Delta);
					TryAddPriceChange(security, time, prevChanges, newChanges, requiredTypes, SecurityChangeTypes.Gamma, security.Gamma);
					TryAddPriceChange(security, time, prevChanges, newChanges, requiredTypes, SecurityChangeTypes.Vega, security.Vega);
					TryAddPriceChange(security, time, prevChanges, newChanges, requiredTypes, SecurityChangeTypes.Theta, security.Theta);

					TryAddPriceChange(security, time, prevChanges, newChanges, requiredTypes, SecurityChangeTypes.MarginBuy, security.MarginBuy);
					TryAddPriceChange(security, time, prevChanges, newChanges, requiredTypes, SecurityChangeTypes.MarginSell, security.MarginSell);
					
					TryAddPriceChange(security, time, prevChanges, newChanges, requiredTypes, SecurityChangeTypes.OpenInterest, security.OpenInterest);
					TryAddPriceChange(security, time, prevChanges, newChanges, requiredTypes, SecurityChangeTypes.MinStepPrice, security.MinStepPrice);

					TryAddPriceChange(security, time, prevChanges, newChanges, requiredTypes, SecurityChangeTypes.BidsCount, security.BidsCount);
					TryAddPriceChange(security, time, prevChanges, newChanges, requiredTypes, SecurityChangeTypes.BidsVolume, security.BidsVolume);
					TryAddPriceChange(security, time, prevChanges, newChanges, requiredTypes, SecurityChangeTypes.AsksCount, security.AsksCount);
					TryAddPriceChange(security, time, prevChanges, newChanges, requiredTypes, SecurityChangeTypes.AsksVolume, security.AsksVolume);

					TryAddPriceChange(security, time, prevChanges, newChanges, requiredTypes, SecurityChangeTypes.BestBid, security.BestBid);
					TryAddPriceChange(security, time, prevChanges, newChanges, requiredTypes, SecurityChangeTypes.BestAsk, security.BestAsk);

					if (requiredTypes.Contains(SecurityChangeTypes.LastTrade))
					{
						var lastTrade = security.LastTrade;

						if (lastTrade != null && lastTrade.Price != 0)
						{
							var prevTrade = (Trade)prevChanges.TryGetValue(SecurityChangeTypes.LastTrade);

							if (prevTrade == null || lastTrade.Price != prevTrade.Price || lastTrade.Volume != prevTrade.Volume)
							{
								lastTrade = lastTrade.Clone();

								prevChanges[SecurityChangeTypes.LastTrade] = lastTrade;

								newChanges.Add(new SecurityChange(security, time, SecurityChangeTypes.LastTrade, lastTrade));
							}
						}
					}
				}
			}

			return newChanges;
		}

		private static void TryAddPriceChange(Security security, DateTime time, SynchronizedDictionary<SecurityChangeTypes, object> prevChanges, List<SecurityChange> newChanges, SecurityChangeTypes requiredTypes, SecurityChangeTypes type, decimal price)
		{
			if (!requiredTypes.Contains(type))
				return;

			if (price == 0)
				return;

			var prevPrice = prevChanges.TryGetValue(type);

			if (prevPrice != null && price == (decimal)prevPrice)
				return;

			prevChanges[type] = price;
			newChanges.Add(new SecurityChange(security, time, type, price));
		}

		private static void TryAddPriceChange(Security security, DateTime time, SynchronizedDictionary<SecurityChangeTypes, object> prevChanges, List<SecurityChange> newChanges, SecurityChangeTypes requiredTypes, SecurityChangeTypes type, Quote quote)
		{
			if (!requiredTypes.Contains(type))
				return;

			if (quote == null || quote.Price == 0)
				return;

			var prevQuote = (Quote)prevChanges.TryGetValue(type);

			if (prevQuote != null && quote.Price == prevQuote.Price && quote.Volume == prevQuote.Volume)
				return;

			quote = quote.Clone();

			prevChanges[type] = quote;

			newChanges.Add(new SecurityChange(security, time, type, quote));
		}

		/// <summary>
		/// Применить изменения к инструменту.
		/// </summary>
		/// <param name="security">Инструмент.</param>
		/// <param name="changes">Изменения.</param>
		public static void ApplyChanges(this Security security, IEnumerable<SecurityChange> changes)
		{
			if (security == null)
				throw new ArgumentNullException("security");

			if (changes == null)
				throw new ArgumentNullException("changes");

			foreach (var change in changes)
			{
				switch (change.Type)
				{
					case SecurityChangeTypes.OpenPrice:
						security.OpenPrice = (decimal)change.Value;
						break;
					case SecurityChangeTypes.HighPrice:
						security.HighPrice = (decimal)change.Value;
						break;
					case SecurityChangeTypes.LowPrice:
						security.LowPrice = (decimal)change.Value;
						break;
					case SecurityChangeTypes.ClosePrice:
						security.ClosePrice = (decimal)change.Value;
						break;
					case SecurityChangeTypes.LastTrade:
						security.LastTrade = (Trade)change.Value;
						break;
					case SecurityChangeTypes.MinStepPrice:
						security.MinStepPrice = (decimal)change.Value;
						break;
					case SecurityChangeTypes.MinStepSize:
						security.MinStepSize = (decimal)change.Value;
						break;
					case SecurityChangeTypes.BestBid:
						security.BestBid = (Quote)change.Value;
						break;
					case SecurityChangeTypes.BestAsk:
						security.BestAsk = (Quote)change.Value;
						break;
					case SecurityChangeTypes.ImpliedVolatility:
						security.ImpliedVolatility = (decimal)change.Value;
						break;
					case SecurityChangeTypes.HistoricalVolatility:
						security.HistoricalVolatility = (decimal)change.Value;
						break;
					case SecurityChangeTypes.TheorPrice:
						security.TheorPrice = (decimal)change.Value;
						break;
					case SecurityChangeTypes.Delta:
						security.Delta = (decimal)change.Value;
						break;
					case SecurityChangeTypes.Gamma:
						security.Gamma = (decimal)change.Value;
						break;
					case SecurityChangeTypes.Vega:
						security.Vega = (decimal)change.Value;
						break;
					case SecurityChangeTypes.Theta:
						security.Theta = (decimal)change.Value;
						break;
					case SecurityChangeTypes.MarginBuy:
						security.MarginBuy = (decimal)change.Value;
						break;
					case SecurityChangeTypes.MarginSell:
						security.MarginSell = (decimal)change.Value;
						break;
					case SecurityChangeTypes.OpenInterest:
						security.OpenInterest = (decimal)change.Value;
						break;
					case SecurityChangeTypes.MinPrice:
						security.MinPrice = (decimal)change.Value;
						break;
					case SecurityChangeTypes.MaxPrice:
						security.MaxPrice = (decimal)change.Value;
						break;
					case SecurityChangeTypes.BidsCount:
						security.BidsCount = (decimal)change.Value;
						break;
					case SecurityChangeTypes.BidsVolume:
						security.BidsVolume = (decimal)change.Value;
						break;
					case SecurityChangeTypes.AsksCount:
						security.AsksCount = (decimal)change.Value;
						break;
					case SecurityChangeTypes.AsksVolume:
						security.AsksVolume = (decimal)change.Value;
						break;
					default:
						throw new ArgumentOutOfRangeException();
				}
			}
		}

		/// <summary>
		/// Получить для хранилища <see cref="IMarketDataStorageDrive"/>.
		/// </summary>
		/// <param name="storage">Хранилище.</param>
		/// <returns><see cref="IMarketDataStorageDrive"/>, ассоциированный с хранилищем.</returns>
		public static IMarketDataStorageDrive GetStorageDrive(this IMarketDataStorage storage)
		{
			if (storage == null)
				throw new ArgumentNullException("storage");

			return storage.Drive.GetStorageDrive(storage.Security, storage.DataType, storage.Arg);
		}
	}
}